
# -*- coding: utf-8 -*-


import gurobipy as gr
import numpy as np
import json
import root_const
from root_const import ROOT_PATH
from base_utils.my_utils import save_json, load_json
from server_placement.server_placement_basic import ServerPlacementBasic

class ServerPlacementILP(ServerPlacementBasic):
    
    """
    Sub class of server_place : using integer linear programming 
    """
    
    def __init__(self, location_array, k_near, k_relative, compute_num, workload, adjust_rate):
        """
        
        param
        ----------
            location_array : array
                the array to store both the lantitude and longtitude information of the location
                structure : nx2
                content : first column ---> lantitude , second column ---> longitude
            k_near : int
                the number of the near base station we will find
            compute_num : int
                the number of the servers we want to assign
            workload : list
                store the information about the load
        """
        ServerPlacementBasic._init_(self, compute_num)
        self.location_array = location_array
        self.k_near = k_near
        self.workload = list(np.array(workload) / adjust_rate)
        self.k_relative = k_relative
    
    def find_near_loca(self, distance):
        near_loca_num = []
        for i in range(len(self.location_array)):
            location_num = self.search_base_indistance(self.location_array[i], self.location_array, distance)
            near_loca_num.append(location_num)
        return near_loca_num
    
    def find_same_receiver(self, list1, list2):
        list3 = list(set(list1) & set(list2))
        return list3
    
    def find_same_receiver_union(self):
        list_near, list_far, list_assign = self.location_divide()
        base_num = len(list_near)
        all_result = []
        for i in range(base_num):
            if i % 100 == 0:
                print(i)
            all_result.append(dict())
            base_list = list_near[i]
            for j in range(base_num):
                same_result = self.find_same_receiver(base_list,list_near[j])
                for k in range(len(same_result)):
                    key = str(same_result[k])
                    if key in all_result[i].keys():
                        all_result[i][key] = all_result[i][key] + 1
                    else:
                        all_result[i][key] = 0
        path = ROOT_PATH + '/all_data/server_scheduling_result/server_union_statistic/'
        name = 'server_union.json'
        save_json(path, name, all_result)
        
    def find_relative_loca(self):
        union_relative = load_json(ROOT_PATH + '/all_data/server_scheduling_result/server_union_statistic/', 
                          'server_union.json')
        list_relative = []
        list_relative_assign = []
        for i in range(len(union_relative)):
            list_relative_assign.append([])    
        for i in range(len(union_relative)):
            dict_temp = union_relative[i]
            dict_sort = sorted(dict_temp.items(),key=lambda dict_temp:dict_temp[1],reverse=True)
            dict_key = [dict_sort[i][0] for i in range(len(dict_sort))]
            dict_key = list(map(int, dict_key))
            list_relative.append(dict_key[0:self.k_relative])
            for j in range(self.k_relative):
                list_relative_assign[list_relative[i][j]].append(i)
        return list_relative, list_relative_assign
    
    def search_relative_weight(self):
        union_relative = load_json(ROOT_PATH + '/all_data/server_scheduling_result/server_union_statistic/', 
                                   'server_union.json')
        node_frequ_statistic = [0 for i in range(len(self.location_array))]
        for i in range(len(union_relative)):
            dict_temp = union_relative[i]
            for k,v in dict_temp.items():
                node_frequ_statistic[int(k)] = node_frequ_statistic[int(k)] + v
        node_frequ_statistic = list(np.array(node_frequ_statistic) * 2/(max(node_frequ_statistic) + min(node_frequ_statistic)))
        return node_frequ_statistic
    
            
    def location_divide(self):
        """find the near k base_station and get their index
        
        param
        ----------
            location_array : array
                the array to store both the lantitude and longtitude information of the location
                structure : nx2
                content : first column ---> lantitude , second column ---> longitude
            k_near : int
                the number of the near base station we will find
        
        return
        ----------
            list_near : list
                store the k near base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the k near base stations
            list_far : list
                store the other base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the other base stations
            list_assign : list
                store the index of which station provide the flow to one base station
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the indexes of base stations that share the flows to one base station according to the index
        
        refer
        ----------
            Other func :  self.array_calculate_distance
        """
        base_num = len(self.location_array)
        list_near = list()
        list_far = list()
        list_assign = []
            
        for i in range(base_num):
            list_assign.append([])
                
        for index in range(base_num):
            base_index = self.location_array[index]
            base_all = np.ones((base_num, 1)) * base_index
            base_distance = self.array_calculate_distance(base_all, self.location_array)
            distance_sort_index = np.argsort(base_distance)
            distance_near = list(distance_sort_index[0 : self.k_near])
            distance_far = list(distance_sort_index[self.k_near : base_num])
            list_near.append(distance_near)
            list_far.append(distance_far)
            for i in range(self.k_near):
                list_assign[distance_near[i]].append(index)
        return list_near, list_far, list_assign
        
    def server_placement(self):
        """we use the ilp model to test load balance function of each formulation
        
        param
        ----------
            list_near : list
                store the k near base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the k near base stations
            list_far : list
                store the other base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the other base stations
            list_assign : list
                store the index of which station provide the flow to one base station
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the indexes of base stations that share the flows to one base station according to the index
            self.k_place : int
                The number of the servers we want to assign
            self.workload : list
                Store the list of the workload about each base
                
        return
        ----------
            u_list : array
               store the proportion of the assignment in each base station
            y_list : array
               store the distribution of server in each base station
            final_optimal : 1/beta 
               parameter to describe the load balance function

        refer
        ----------
            Other func :  None
            Document : The introduction document of gurobi
        """
        list_near, list_far, list_assign = self.location_divide()
        base_num = len(list_near)
        list_var = []
            
        model_placement = gr.Model('Lip_placement')
        u = model_placement.addVars(base_num, len(list_near[0]), vtype='S', lb=0)                 
        y = model_placement.addVars(base_num, vtype='I', lb=0)                                           
        a = model_placement.addVar(vtype='S', lb=0)                                                     
        model_placement.setObjective(a,gr.GRB.MAXIMIZE)
            
        model_placement.addConstr(
                sum(y[i] for i in range(base_num)) == self.compute_num
        )  
        model_placement.addConstrs(
                sum(u[i,j] for j in range(len(list_near[0]))) == a 
                for i in range(base_num)
        )
        model_placement.addConstrs(
                sum(u[list_assign[i][j],list_near[list_assign[i][j]].index(i)]*self.workload[list_assign[i][j]] 
                for j in range(len(list_assign[i]))) <=y[i] 
                for i in range(base_num)
        )
        model_placement.optimize()
            
        if model_placement.Status == gr.GRB.OPTIMAL:
            for var in model_placement.getVars():
                list_var.append(var.x)
            u_list = np.array(list_var[0 : base_num*len(list_near[0])])
            y_list = np.array(list_var[base_num*len(list_near[0]) : base_num*len(list_near[0]) + base_num])
            u_list = u_list.reshape((base_num, len(list_near[0])))
            final_optimal = list_var[base_num * len(list_near[0]) + base_num]
        return u_list, y_list, final_optimal
    
    def server_placement_relative(self):
        """we use the ilp model to test load balance function of each formulation
        
        param
        ----------
            list_near : list
                store the k near base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the k near base stations
            list_far : list
                store the other base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the other base stations
            list_assign : list
                store the index of which station provide the flow to one base station
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the indexes of base stations that share the flows to one base station according to the index
            self.k_place : int
                The number of the servers we want to assign
            self.workload : list
                Store the list of the workload about each base
                
        return
        ----------
            u_list : array
               store the proportion of the assignment in each base station
            y_list : array
               store the distribution of server in each base station
            final_optimal : 1/beta 
               parameter to describe the load balance function

        refer
        ----------
            Other func :  None
            Document : The introduction document of gurobi
        """
#        list_near, list_far, list_assign = self.location_divide()
        list_near, list_assign = self.find_relative_loca()
        base_num = len(list_near)
        list_var = []
            
        model_placement = gr.Model('Lip_placement')
        u = model_placement.addVars(base_num, len(list_near[0]), vtype='S', lb=0)                 
        y = model_placement.addVars(base_num, vtype='I', lb=0)                                           
        a = model_placement.addVar(vtype='S', lb=0)                                                     
        model_placement.setObjective(a,gr.GRB.MAXIMIZE)
            
        model_placement.addConstr(
                sum(y[i] for i in range(base_num)) == self.compute_num
        )  
        model_placement.addConstrs(
                sum(u[i,j] for j in range(len(list_near[0]))) == a 
                for i in range(base_num)
        )
        model_placement.addConstrs(
                sum(u[list_assign[i][j],list_near[list_assign[i][j]].index(i)]*self.workload[list_assign[i][j]] 
                for j in range(len(list_assign[i]))) <=y[i] 
                for i in range(base_num)
        )
        model_placement.optimize()
            
        if model_placement.Status == gr.GRB.OPTIMAL:
            for var in model_placement.getVars():
                list_var.append(var.x)
            u_list = np.array(list_var[0 : base_num*len(list_near[0])])
            y_list = np.array(list_var[base_num*len(list_near[0]) : base_num*len(list_near[0]) + base_num])
            u_list = u_list.reshape((base_num, len(list_near[0])))
            final_optimal = list_var[base_num * len(list_near[0]) + base_num]
        return u_list, y_list, final_optimal
    
    def server_placement_float(self):
        """we use the ilp model to test load balance function of each formulation
        
        param
        ----------
            list_near : list
                store the k near base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the k near base stations
            list_far : list
                store the other base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the other base stations
            list_assign : list
                store the index of which station provide the flow to one base station
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the indexes of base stations that share the flows to one base station according to the index
            self.k_place : int
                The number of the servers we want to assign
            self.workload : list
                Store the list of the workload about each base
                
        return
        ----------
            u_list : array
               store the proportion of the assignment in each base station
            y_list : array
               store the distribution of server in each base station
            final_optimal : 1/beta 
               parameter to describe the load balance function

        refer
        ----------
            Other func :  None
            Document : The introduction document of gurobi
        """
        list_near, list_far, list_assign = self.location_divide()
        base_num = len(list_near)
        list_var = []
            
        model_placement = gr.Model('Lip_placement')
        u = model_placement.addVars(base_num, len(list_near[0]), vtype='S', lb=0)                 
        y = model_placement.addVars(base_num, vtype='S', lb=0)                                           
        a = model_placement.addVar(vtype='S', lb=0)                                                     
        model_placement.setObjective(a,gr.GRB.MAXIMIZE)
            
        model_placement.addConstr(
                sum(y[i] for i in range(base_num)) == self.compute_num
        )  
        model_placement.addConstrs(
                sum(u[i,j] for j in range(len(list_near[0]))) == a 
                for i in range(base_num)
        )
        model_placement.addConstrs(
                sum(u[list_assign[i][j],list_near[list_assign[i][j]].index(i)]*self.workload[list_assign[i][j]] 
                for j in range(len(list_assign[i]))) <=y[i] 
                for i in range(base_num)
        )
        model_placement.optimize()
            
        if model_placement.Status == gr.GRB.OPTIMAL:
            for var in model_placement.getVars():
                list_var.append(var.x)
            u_list = np.array(list_var[0 : base_num*len(list_near[0])])
            y_list = np.array(list_var[base_num*len(list_near[0]) : base_num*len(list_near[0])+base_num])
            u_list = u_list.reshape((base_num, len(list_near[0])))
            final_optimal = list_var[base_num * len(list_near[0]) + base_num]
        u_list_real = list()
        for i in range(len(u_list)):
            list_new = [0 for i in range(len(u_list))]
            for j in range(len(list_near[i])):
                list_new[list_near[i][j]] = u_list[i][j]
            u_list_real.append(list_new)
        return u_list_real, y_list, final_optimal

    def server_placement_float_best(self, final_optimal, distance):
        """we use the ilp model to test load balance function of each formulation
        
        param
        ----------
            list_near : list
                store the k near base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the k near base stations
            list_far : list
                store the other base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the other base stations
            list_assign : list
                store the index of which station provide the flow to one base station
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the indexes of base stations that share the flows to one base station according to the index
            self.k_place : int
                The number of the servers we want to assign
            self.workload : list
                Store the list of the workload about each base
                
        return
        ----------
            u_list : array
               store the proportion of the assignment in each base station
            y_list : array
               store the distribution of server in each base station
            final_optimal : 1/beta 
               parameter to describe the load balance function

        refer
        ----------
            Other func :  None
            Document : The introduction document of gurobi
        """
        list_near, list_far, list_assign = self.location_divide()
#        node_frequ_statistic = self.search_relative_weight()
        node_frequ_statistic = self.find_near_loca(distance)
        base_num = len(list_near)
        list_var = []
            
        model_placement = gr.Model('Lip_placement')
        u = model_placement.addVars(base_num, len(list_near[0]), vtype='S', lb=0)                 
        y = model_placement.addVars(base_num, vtype='S', lb=0)                                           
#        a = model_placement.addVar(vtype='S', lb=0)                                                     
        model_placement.setObjective(sum(node_frequ_statistic[i] * y[i] for i in range(base_num)),gr.GRB.MAXIMIZE)
            
        model_placement.addConstr(
                sum(y[i] for i in range(base_num)) == self.compute_num
        )  
        model_placement.addConstrs(
                sum(u[i,j] for j in range(len(list_near[0]))) == final_optimal
                for i in range(base_num)
        )
        model_placement.addConstrs(
                sum(u[list_assign[i][j],list_near[list_assign[i][j]].index(i)]*self.workload[list_assign[i][j]] 
                for j in range(len(list_assign[i]))) <=y[i] 
                for i in range(base_num)
        )
        model_placement.optimize()
            
        if model_placement.Status == gr.GRB.OPTIMAL:
            for var in model_placement.getVars():
                list_var.append(var.x)
            u_list = np.array(list_var[0 : base_num*len(list_near[0])])
            y_list = np.array(list_var[base_num*len(list_near[0]) : base_num*len(list_near[0])+base_num])
            u_list = u_list.reshape((base_num, len(list_near[0])))
        u_list_real = list()
        for i in range(len(u_list)):
            list_new = [0 for i in range(len(u_list))]
            for j in range(len(list_near[i])):
                list_new[list_near[i][j]] = u_list[i][j]
            u_list_real.append(list_new)
        return u_list_real, y_list
    
    def server_placement_float_best2(self, final_optimal):
 
        
        list_near, list_far, list_assign = self.location_divide()
#        node_frequ_statistic = self.search_relative_weight()
        base_num = len(list_near)
        list_var = []
            
        model_placement = gr.Model('Lip_placement')
        u = model_placement.addVars(base_num, len(list_near[0]), vtype='S', lb=0)                 
        y = model_placement.addVars(base_num, vtype='S', lb=0)                                           
        a = model_placement.addVar(vtype='S', lb=1)                                                     
        model_placement.setObjective(a , gr.GRB.MAXIMIZE)
        model_placement.addConstr(
                sum(y[i] for i in range(base_num)) == self.compute_num
        )  
        model_placement.addConstrs(
                sum(y[list_near[k][i]] for i in range(len(list_near[k]))) >= a * self.workload[k] 
                for k in range(len(list_near))
        )
        model_placement.addConstrs(
                sum(u[i,j] for j in range(len(list_near[0]))) == final_optimal
                for i in range(base_num)
        )
        model_placement.addConstrs(
                sum(u[list_assign[i][j],list_near[list_assign[i][j]].index(i)]*self.workload[list_assign[i][j]] 
                for j in range(len(list_assign[i]))) <=y[i] 
                for i in range(base_num)
        )
        model_placement.optimize()
            
        if model_placement.Status == gr.GRB.OPTIMAL:
            for var in model_placement.getVars():
                list_var.append(var.x)
            u_list = np.array(list_var[0 : base_num*len(list_near[0])])
            y_list = np.array(list_var[base_num*len(list_near[0]) : base_num*len(list_near[0])+base_num])
            u_list = u_list.reshape((base_num, len(list_near[0])))
        u_list_real = list()
        for i in range(len(u_list)):
            list_new = [0 for i in range(len(u_list))]
            for j in range(len(list_near[i])):
                list_new[list_near[i][j]] = u_list[i][j]
            u_list_real.append(list_new)
        return u_list_real, y_list
   
    def server_placement_float_relative(self):
        """we use the ilp model to test load balance function of each formulation
        
        param
        ----------
            list_near : list
                store the k near base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the k near base stations
            list_far : list
                store the other base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the other base stations
            list_assign : list
                store the index of which station provide the flow to one base station
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the indexes of base stations that share the flows to one base station according to the index
            self.k_place : int
                The number of the servers we want to assign
            self.workload : list
                Store the list of the workload about each base
                
        return
        ----------
            u_list : array
               store the proportion of the assignment in each base station
            y_list : array
               store the distribution of server in each base station
            final_optimal : 1/beta 
               parameter to describe the load balance function

        refer
        ----------
            Other func :  None
            Document : The introduction document of gurobi
        """
        list_near, list_assign = self.find_relative_loca()
        base_num = len(list_near)
        list_var = []
            
        model_placement = gr.Model('Lip_placement')
        u = model_placement.addVars(base_num, len(list_near[0]), vtype='S', lb=0)                 
        y = model_placement.addVars(base_num, vtype='S', lb=0)                                           
        a = model_placement.addVar(vtype='S', lb=0)                                                     
        model_placement.setObjective(a,gr.GRB.MAXIMIZE)
            
        model_placement.addConstr(
                sum(y[i] for i in range(base_num)) == self.compute_num
        )  
        model_placement.addConstrs(
                sum(u[i,j] for j in range(len(list_near[0]))) == a 
                for i in range(base_num)
        )
        model_placement.addConstrs(
                sum(u[list_assign[i][j],list_near[list_assign[i][j]].index(i)]*self.workload[list_assign[i][j]] 
                for j in range(len(list_assign[i]))) <=y[i] 
                for i in range(base_num)
        )
        model_placement.optimize()
            
        if model_placement.Status == gr.GRB.OPTIMAL:
            for var in model_placement.getVars():
                list_var.append(var.x)
            u_list = np.array(list_var[0 : base_num*len(list_near[0])])
            y_list = np.array(list_var[base_num*len(list_near[0]) : base_num*len(list_near[0])+base_num])
            u_list = u_list.reshape((base_num, len(list_near[0])))
            final_optimal = list_var[base_num * len(list_near[0]) + base_num]
        u_list_real = list()
        for i in range(len(u_list)):
            list_new = [0 for i in range(len(u_list))]
            for j in range(len(list_near[i])):
                list_new[list_near[i][j]] = u_list[i][j]
            u_list_real.append(list_new)
        return u_list_real, y_list, final_optimal
    
